import type {
  ComponentStyleIdType,
  PlatformTypeValue,
} from '../../../../types/interfaces'
import { ScriptNotification } from '../../../../components/notification'
import { elementConfig } from '../../../../element/config/element_config'
import { ElementTag } from '../../../../element/config/element_tag'
import { WebElement } from '../../../../element/config/web_element'
import { BaseModule } from '../../../../lib/abstract'
import { DragHandler } from '../../../../lib/drag_handler'
import { CSDN_ARTICLE_TOC_SELECTOR } from '../../../../lib/page_handler/platform/csdn/types'
import { PollingHandler } from '../../../../lib/polling_handler'
import { ToolKit } from '../../../../lib/utils/toolkit'
import {
  ComponentStyleId,
  HEADING_TAG,
  HOSTNAME,
  LOCAL_STORAGE_KEYS,
  logger,
  SUPPORTED_PLATFORMS,
} from '../../../../types/constants'
import { ConfigManager } from '../../../config_manager'
import { ModuleManager } from '../../../module_manager'
import { StyleManager } from '../../../style_manager'
import { styles } from './styles'

interface Config {
  enabled?: boolean
}

interface PositionInfo {
  position: number
  hidden: boolean
}

export interface TocStatus {
  code: 'NO_ELEMENT' | 'NO_HEADINGS' | 'NO_CONFIG' | 'HOST_UNSUPPORTED' | 'GENERATED' | 'DISABLED'
  icon: string
  message: string
  color: string
}

export const TOC_STATUSES: Record<string, TocStatus> = {
  NO_CONFIG: {
    code: 'NO_CONFIG',
    icon: '⚙️',
    message: '未配置文章标签，跳过目录生成',
    color: 'warn',
  },
  HOST_UNSUPPORTED: {
    code: 'HOST_UNSUPPORTED',
    icon: '🌐',
    message: `${HOSTNAME} 未配置，跳过目录生成`,
    color: 'warn',
  },
  DISABLED: {
    code: 'DISABLED',
    icon: '🔕',
    message: '目录功能已禁用',
    color: 'warn',
  },
  NO_ELEMENT: {
    code: 'NO_ELEMENT',
    icon: '⚠️',
    message: '未找到文章元素，跳过目录生成',
    color: 'warn',
  },
  NO_HEADINGS: {
    code: 'NO_HEADINGS',
    icon: '📄',
    message: '文章无标题，跳过目录生成',
    color: 'warn',
  },
  GENERATED: {
    code: 'GENERATED',
    icon: '📑',
    message: '文章目录已生成',
    color: 'success',
  },
}

export interface TocConfig {
  menu: {
    width: string
    right: string
    top: string
    zIndex: number
    borderRadius: string
    boxShadow: string
  }
  hr: {
    height: string
    color: string
  }
  listItem: {
    fontSize: string
    hoverTransform: string
  }
}

/**
 * 目录引擎（单例模式）
 */
export class TocEngine extends BaseModule {
  readonly id: string = 'toc_engine'
  readonly styleId: string = ComponentStyleId[this.name]
  readonly isInitialized: boolean = false
  readonly supportedPlatforms: string[] = ConfigManager.getInstance().moduleSupportedPlatforms(this.name)
  private static instance: TocEngine
  private readonly elementTag: ElementTag = ElementTag.getInstance()
  private readonly webElement: WebElement = WebElement.getInstance()
  // 页面宽高
  private readonly winWidth = window.innerWidth
  private readonly winHeight = window.innerHeight
  // 拖拽存储键名
  private readonly dragStorageKey = LOCAL_STORAGE_KEYS.Toc
  // 标题起始索引
  private titleIndex = 1
  // 目录标题
  private readonly locTitle = '📖 目录'
  private readonly initialPositions = new Map<string, PositionInfo>()
  private currentStatus: TocStatus = TOC_STATUSES.DISABLED

  private readonly titleBlackSelectors: Record<PlatformTypeValue, string[]> = {
    [SUPPORTED_PLATFORMS.CSDN]: [CSDN_ARTICLE_TOC_SELECTOR],
  }

  readonly sideMenuLoc = {
    name_en: 'side-menu-toc',
    selector: '#side-menu-toc',
  }

  private readonly menuLocOl = {
    name_en: 'menu_toc_ol',
    selector: '#menu_toc_ol',
  }

  config: Config = {
    enabled: true,
  }

  readonly tocConfig: TocConfig = {
    // 目录样式
    menu: {
      width: 'clamp(250px, 25vw, 300px)',
      right: this.getInitialRightPosition(),
      top: this.getInitialTopPosition(),
      zIndex: 100,
      borderRadius: '8px',
      boxShadow: '0 2px 12px rgba(0,0,0,0.1)',
    },
    // 分割线样式
    hr: {
      height: '2px',
      color: '#c7254e',
    },
    // 目录项样式
    listItem: {
      fontSize: '15px',
      hoverTransform: 'translateX(5px)',
    },
  }

  get name(): ComponentStyleIdType {
    return 'TocEngine'
  }

  constructor() {
    super()
  }

  /**
   * 获取单例
   * @returns {TocEngine} 单例
   */
  public static getInstance(): TocEngine {
    if (!TocEngine.instance) {
      TocEngine.instance = new TocEngine()
    }
    return TocEngine.instance
  }

  /**
   * 销毁模块
   */
  destroy(): Promise<void> | void {}

  /**
   * 初始化目录
   */
  public core(): void {
    this.loadConfig()

    if (!this.config.enabled) {
      this.setStatus('DISABLED')
      return
    }

    if (!this.supportedPlatforms.includes(HOSTNAME)) {
      this.setStatus('HOST_UNSUPPORTED')
      return
    }

    // 检查是否已生成目录
    const $toc = $(`#${this.sideMenuLoc.name_en}`)
    if ($toc.length)
      return

    // 检查元素标签配置
    const elementTags = this.elementTag.getSelectors()
    if (elementTags.length === 0) {
      this.setStatus('NO_CONFIG')
      return
    }

    // 页面元素
    const selectorInfo = this.elementTag.getSelector('文章')
    const { tag: articleTag = 'article', listen = false } = selectorInfo || {}

    const handler = () => {
      const $articleElement = $(articleTag)
      // 获取所有标题，包含一级标题到六级标题
      const $titles = this.getTitleElements($articleElement)

      // 检查标题是否存在
      if (!$titles.length) {
        this.setStatus('NO_HEADINGS')
        return
      }

      if (!this.initStructure($articleElement))
        return

      // 生成目录项
      this.generateItems($titles)
      this.bindEvents($titles)
      this.renderDrag()

      this.setStatus('GENERATED')
      logger.info(`[${this.name}] Module initialized`)
    }

    if (!listen) {
      handler()
    }
    else {
      const pollingHandler = new PollingHandler()
      pollingHandler.api.awaitElement(articleTag, handler)
    }
  }

  renderDrag(): void {
    const dragHandler = new DragHandler({
      elementConfig: {
        containerSelector: this.sideMenuLoc.selector,
        dragTargetSelector: `h2`,
      },
      storageKey: this.dragStorageKey,
    })
    dragHandler.init()
  }

  /**
   * 获取所有标题元素
   */
  getTitleElements($articleElement: JQuery<HTMLElement>): JQuery<HTMLElement> {
    const blackSelectors = this.titleBlackSelectors[HOSTNAME] || []
    const $titles = $articleElement.find(HEADING_TAG)

    return blackSelectors.length
      ? $titles.filter((_, el) => !ToolKit.ui.element.isInSelectorList(el, blackSelectors))
      : $titles
  }

  /**
   * 绑定事件
   */
  bindEvents($elements: JQuery<HTMLElement>): void {
    // 绑定点击事件
    this.bindClickEvents()

    // 加载/调整大小/滚动时触发（严格模式）
    const scrollHandler = ToolKit.func.debounce(
      () => this.bindScrollEvents($elements, true),
      100,
    )

    // 滚动时触发，用于同步菜单滚动位置
    const syncHandler = ToolKit.func.debounce(
      () => this.syncMenuScroll(),
      100,
    )

    window.addEventListener('load', scrollHandler, { once: true })
    window.addEventListener('resize', scrollHandler)
    window.addEventListener('scroll', scrollHandler)
    window.addEventListener('scroll', syncHandler)
  }

  async onToggleCallback(_id: string, checked: boolean): Promise<void> {
    ToolKit.dom.simpleToggleElement(checked, '#side-menu-toc')
    checked && await ModuleManager.getInstance().initModule(this.name, () => $('#side-menu-toc').length > 0)
  }

  // 显示提示框
  private showTooltip() {
    const notification = new ScriptNotification()
    notification.show({
      title: '目录生成状态',
      text: `${this.currentStatus.icon} ${this.currentStatus.message}`,
      timeout: 5000,
      highlight: true,
      onClick: () => {},
    })
  }

  /**
   * 更新目录状态
   */
  private setStatus(statusCode: keyof typeof TOC_STATUSES) {
    this.currentStatus = TOC_STATUSES[statusCode]
    this.logStatus()

    if (statusCode === 'GENERATED')
      return

    this.showTooltip()
  }

  /**
   * 获取当前状态
   * @returns {TocStatus} 当前状态
   */
  public getStatus(): TocStatus {
    return this.currentStatus
  }

  // 日志记录器
  private logStatus() {
    const { icon, message, color } = this.currentStatus
    logger[color === 'warn' ? 'warn' : 'info'](
      `[TOC] ${icon} ${message}`,
    )
  }

  /**
   * 获取初始右偏移量
   * @returns {string} 初始右偏移量
   */
  private getInitialRightPosition(): string {
    const cache = localStorage.getItem(this.dragStorageKey)
    const defaultRight = `max(20px, calc(50% - ${elementConfig.getConfig().rightOffset}px))`
    if (!cache)
      return defaultRight

    try {
      const { right } = JSON.parse(cache)
      return `${Math.max(100, Math.min(right, this.winWidth))}px`
    }
    catch {
      return defaultRight
    }
  }

  /**
   * 获取初始顶部偏移量
   * @returns {string} 初始顶部偏移量
   */
  private getInitialTopPosition(): string {
    const cache = localStorage.getItem(this.dragStorageKey)
    const defaultTop = `${elementConfig.getConfig().topOffset}px`
    if (!cache)
      return defaultTop

    try {
      const { top } = JSON.parse(cache)
      return `${Math.max(100, Math.min(top, this.winHeight))}px`
    }
    catch {
      return defaultTop
    }
  }

  getStyles(): string {
    return styles(this.sideMenuLoc.name_en, this.menuLocOl.name_en, this.tocConfig)
  }

  /**
   * 获取排序后的唯一层级
   * @param titles 标题元素
   * @returns {number[]} 排序后的唯一层级
   */
  private getSortedUniqueLevels(titles: JQuery<HTMLElement>): number[] {
    const levelSet = new Set<number>()
    titles.each(function () {
      levelSet.add(Number.parseInt(this.tagName.substring(1)))
    })
    // 将 Set 转为数组并按 h1-h6 顺序排序
    return Array.from(levelSet).sort((a, b) => a - b)
  }

  /**
   * 初始化结构
   * @param articleElement 文章元素
   * @returns {boolean} 是否初始化成功
   */
  private initStructure(articleElement: JQuery<HTMLElement>): boolean {
    if (!articleElement.length) {
      this.setStatus('NO_ELEMENT')
      return false
    }

    // 注册样式
    StyleManager.getInstance().registerStyle(this.styleId, this.getStyles(), false, true)

    // 插入目录
    articleElement.prepend(`
      <div id="${this.sideMenuLoc.name_en}">
        <div class="content-controls">
          <span class="close-btn" title="临时关闭文章目录">&#10005;</span>
        </div>
        <div class="title">
          <h2>
            ${this.locTitle}
          </h2>
          <hr>
        </div>
        <div class="content">
          <ol id="${this.menuLocOl.name_en}"></ol>
        </div>
      </div>
    `)

    // 标记为项目元素
    ToolKit.browser.platform.markProjectElement(articleElement)

    return true
  }

  /**
   * 生成目录项
   * @param titles 标题元素
   */
  private generateItems(titles: JQuery<HTMLElement>): void {
    // 创建文档片段（内存中的临时容器）
    const fragment = document.createDocumentFragment()
    const levels = this.getSortedUniqueLevels(titles)

    titles.each((_, element) => {
      const $title = $(element)
      // 标题级别
      const level = Number.parseInt(element.tagName.substring(1))
      // 标题内容
      const titleText = $title.text()
      // 标题 ID
      const titleId = `toc_${this.titleIndex++}`
      // 为标题添加 ID
      $title.attr('id', titleId)

      // 当前标题在去重后的层级中的索引
      const levelIndex = levels.indexOf(level)
      // 增加默认左边距，美化悬浮显示效果
      // 所有标题层级相同，或标题层级与去重按顺序排序后的第一个层级相同时满足条件
      // 标题层级与去重按顺序排序后的最后一个层级相同时满足条件
      const paddingLeft = levels.length === 1 || levelIndex === 0
        ? 10
        : levelIndex * 20

      if (!titleText.includes(this.locTitle)) {
        // 生成目录项
        const $li = $(`
          <li class="${titleId}" style="
            padding-left: ${paddingLeft}px;
            margin: 6px 0;
            transition: all 0.2s;
            cursor: pointer;
          ">${titleText}</li>
        `)
        // 将原生 DOM 元素添加到片段
        fragment.appendChild($li[0])
      }
    })

    // 一次性添加所有目录项到文档片段，避免频繁操作DOM
    $(this.menuLocOl.selector).append(fragment)
  }

  /**
   * 获取元素绝对位置及其相对于视口的位置状态
   * @param element 目标元素
   * @returns 包含绝对位置和位置状态的对象
   */
  private getElementPositionInfo(element: HTMLElement): {
    absoluteY: number
    viewportPosition: 'above' | 'in' | 'below'
  } {
    const rect = element.getBoundingClientRect()
    const viewportHeight = window.innerHeight
    // 计算绝对位置（相对于文档顶部）: 元素顶部相对于视口顶部的距离 + 页面滚动的距离
    const absoluteY = rect.top + window.pageYOffset

    // 判断元素相对于视口的位置状态
    let position: 'above' | 'in' | 'below'

    if (rect.top < 0 && rect.bottom > 0) {
      // 元素部分在视口中（顶部超出）
      position = 'in'
    }
    else if (rect.bottom < 0) {
      // 元素完全在视口上方
      position = 'above'
    }
    else if (rect.top > viewportHeight) {
      // 元素完全在视口下方
      position = 'below'
    }
    else {
      // 元素完全在视口中
      position = 'in'
    }

    return { absoluteY, viewportPosition: position }
  }

  /**
   * 检查是否影响到 toc 滚动位置的元素是否均隐藏
   * @returns {boolean} 是否隐藏
   */
  private checkTocElementHidden(): boolean {
    const elements = this.webElement.getSelectors() ?? []
    return elements.some(e => e.effectToc)
      && elements
        .filter(e => e.effectToc)
        .every(e => $(e.selector).is(':hidden'))
  }

  /**
   * 计算滚动位置
   * @param element 目标元素
   * @param id 元素 ID
   * @returns 滚动位置
   */
  private calculateScrollPosition(element: HTMLElement, id: string): number {
    const isHidden = this.checkTocElementHidden()
    // 是否已经记录过初始位置
    const isRecorded = this.initialPositions.has(id) && this.initialPositions.get(id)?.hidden === isHidden
    // 记录初始位置（如果尚未记录）
    if (!isRecorded) {
      this.initialPositions.set(id, {
        position: this.getElementPositionInfo(element).absoluteY,
        hidden: isHidden,
      })
    }

    const currentPos = this.getElementPositionInfo(element).absoluteY
    const initialPos = this.initialPositions.get(id)?.position || currentPos
    // 计算布局变化导致的偏移量
    const layoutShift = initialPos - currentPos
    // 顶部预留空间，需要考虑以下几种情况：
    // 1) 页面未发生布局变化时，首次点击
    // 2) 页面未发生布局变化时，第二次点击
    // 3) 页面发生布局变化
    const headerOffset = isHidden === false && (layoutShift === 0 || (isRecorded === true && layoutShift !== 0)) ? 60 : 0

    // 最终位置 = 当前绝对位置 - 顶部边距 + 布局偏移补偿
    return currentPos - headerOffset + layoutShift
  }

  /**
   * 绑定点击事件
   */
  private bindClickEvents(): void {
    const $tocContainer = $(this.sideMenuLoc.selector)
    const $tocMenu = $(this.menuLocOl.selector)

    const $tocLi = $tocMenu.find('li')
    $tocLi.on('click', (e) => {
      // =============================== 处理点击状态 ===============================
      const $li = $(e.currentTarget as HTMLElement)
      // 清除所有元素点击状态
      $tocLi.removeClass('active')
      // 设置当前元素点击状态
      $li.addClass('active')

      // =============================== 处理页面滚动 ===============================
      // 获取目标元素
      const targetId = $li.clone()
        .removeClass((_, className) =>
          className.split(' ')
            .filter(c => !c.startsWith('toc_'))
            .join(' '))
        .attr('class') as string

      const targetElement = $(`#${targetId}`)[0]
      // 多次点击同一目录项时，不做处理
      if (!targetElement)
        return

      // 滚动到目标位置
      window.scrollTo({
        top: this.calculateScrollPosition(targetElement, targetId), // 计算滚动位置
        behavior: 'smooth',
      })
    })

    // 关闭按钮
    const $closeBtn = $tocContainer.find('.close-btn')
    $closeBtn.on('click', () => {
      $tocContainer.hide()
    })
  }

  /**
   * 绑定滚动事件
   * @param titles 标题元素
   * @param strictMode 是否严格模式
   */
  private bindScrollEvents(titles: JQuery<HTMLElement>, strictMode: boolean): void {
    let currentId = ''
    let minVisibleTop = Infinity
    let maxInvisibleTop = -Infinity
    const isHidden = this.checkTocElementHidden()

    strictMode = isHidden ? false : strictMode

    Array.from(titles).forEach((title) => {
      const rect = title.getBoundingClientRect()
      const id = title.getAttribute('id') || ''

      // 严格模式判断逻辑
      const isVisible = strictMode
        ? rect.top >= 10 && rect.bottom <= window.innerHeight
        : rect.top >= -rect.height

      if (isVisible && rect.top < minVisibleTop) {
        minVisibleTop = rect.top
        currentId = id
      }
      else if (rect.top < 0 && rect.top > maxInvisibleTop) {
        maxInvisibleTop = rect.top
        currentId = id
      }
    })

    $(`${this.menuLocOl.selector} li`).each(function () {
      const firstClass = this.className.split(/\s+/)[0] || ''
      $(this).toggleClass('active', firstClass === currentId)
    })
  }

  /**
   * 同步菜单滚动（页面和长目录滚动同步）
   */
  private syncMenuScroll(): void {
    const activeItem = $(`${this.menuLocOl.selector} li.active`)[0]
    if (!activeItem)
      return

    const menu = $(this.sideMenuLoc.selector)[0]
    // 计算目标滚动位置
    const targetScrollTop = activeItem.offsetTop - (menu.clientHeight - activeItem.offsetHeight) / 2

    // 节流平滑滚动
    if (!menu.scrollTimeout) {
      (menu as any).scrollTimeout = setTimeout(() => {
        menu.scrollTo({
          top: targetScrollTop,
          behavior: 'smooth',
        })
        menu.scrollTimeout = undefined
      }, 150) // 调整节流时间 (单位: 毫秒)
    }
  }

  applyConfig(_config: Partial<Config>): void {
  }
}
